Report parser field mismatches instead of trapping - #7
Merged
Conversation
Two uncatchable traps in the fixed-width path: - parsePavementClassification indexed the five components of a PCN value without checking how many the split produced. The Pavement Classification field is widening from 11 to 16 characters for the ICAO PCR transition, and a PCR value is conventionally four-part, so a value from an upcoming cycle would trap on the first runway record carrying one. The call site wraps the parse in do/catch, but an out-of-range subscript is a trap, not a throw, so that handler never ran. - ByteTransformer indexed its compiled-in transformation list by the position of a slice produced from the runtime-parsed layout. A layout that gains a field trapped; one that loses a field silently read every subsequent value from its neighbour. Both now throw, so the parse error handler sees them and the affected record is dropped rather than the process. Airport.id is documented as unique within a single cycle rather than stable across cycles: the FAA re-keyed FAA LID 18AL from site number 03329.19 to 00329.19 in the 2026-09-03 cycle. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QZ3UWydnavm3g9Gs62Xirb
RISCfuture
commented
Sep 8, 2026
| NASR cycle. It is not a stable identifier across cycles: the FAA | ||
| occasionally corrects a site number, just as an airport's ``LID`` can | ||
| change. Persisting either value across cycles requires a reconciliation step | ||
| to detect a record that has been re-keyed rather than retired. |
Owner
Author
There was a problem hiding this comment.
Leave these extra notes out of here -- if anything, they should go into a separate document or other location more focused on cycle updates. For this here, just cover the static info.
| ) | ||
| case let .fieldCountMismatch(expected, actual): | ||
| return String( | ||
| localized: "Layout describes \(actual) fields, but the parser transforms \(expected)" |
Owner
Author
There was a problem hiding this comment.
Use format: .number (where supported on this platform)
The site number's documentation covers what the identifier is and the cycle it is unique within; guidance on handling cycle updates belongs elsewhere. The field count mismatch message renders both counts through `.number` so they localize. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QZ3UWydnavm3g9Gs62Xirb
Foundation on Linux resolves `String(localized:)` through a plain-`String` initializer, whose interpolation takes no format style. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QZ3UWydnavm3g9Gs62Xirb
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two out-of-range array subscripts in the fixed-width parsing path. Both are traps, not throws, so neither can be caught by the
errorHandlerthatNASR.parse(_:withProgress:errorHandler:)is built around: the process dies with SIGILL and the caller gets no diagnostic and no chance to skip the record.The PCN trap is a live risk, not a hypothetical
parsePavementClassification(_:)split the Pavement Classification field on/and indexedcomponents[1]throughcomponents[4]unchecked; onlycomponents[0]was guarded.The call site does wrap the parse:
That
catchcannot run for an out-of-range subscript.What makes this current rather than theoretical: the field widened from 11 to 16 characters in the 2026-09-03 airport layout because it is transitioning from ICAO PCN to ICAO PCR. A PCR string is conventionally four-part, where PCN is the five-part
number/type/subgrade/tirePressure/determination. The first runway record in any cycle that carries a PCR-shaped value in that field would take the process down. Shorter values trap the same way — a bare61, or61//B/X/T, sincesplitdrops empty subsequences by default.The fix guards the component count and throws the
Error.invalidPavementClassification(_:)the function already throws elsewhere, so the existingcatchroutes it toFixedWidthParserError.invalidValue(_:at:)and the record is dropped through the normal error channel.On
omittingEmptySubsequences: false: considered and rejected. It would turn61//B/X/Tinto five components with an empty type, which then fails deeper inClassification.require("")— a worse diagnostic than reporting the whole offending value. Keeping the default split means every malformed shape reports the same way, naming the value the FAA actually published.The CSV path (
CSVAirportParser) reads the five pieces as separate columns and was already safe; it is untouched.The field-count trap is the same failure one layer up
ByteTransformer.applyTo(_:)mapped over slices produced from the FAA's runtime-parsed layout file while indexingfields— the hardcoded, positional transformer list (135 entries for the runway record). Nothing checked that the two agreed:fields[index], a fresh SIGILL of exactly the kind fixed in 4.1.1;Now guarded, throwing a new
FixedWidthParserError.fieldCountMismatch(expected:actual:)that names both counts.Source compatibility:
FixedWidthParserErroris internal, so adding a case is not source-breaking for consumers. Errors reach consumers wrapped in the publicRecordParseError, whose cases are unchanged.Airport.iddocumentationThe site number was documented as the field to use "as the
LIDfor an airport can sometimes change." The FAA falsified that in the 2026-09-03 cycle: FAA LID18AL(LOUISVILLE STAGEFIELD AHP) moved from site number03329.19to00329.19, a corrected digit transposition — permanent, and not re-published under the old key. The site number is now documented as unique within a cycle but not stable across cycles, with the note that persisting either identifier needs a reconciliation step to tell a re-keyed record from a retired one.Testability change
parsePavementClassification(_:)is nowstaticinstead ofprivate. It touches no actor state, so nonisolated-static is the more accurate declaration, and it lets the tests exercise the guard directly rather than standing up anAirportthrough an 80-parameter initializer to reach the runway record path. It moved above the instance methods to satisfy SwiftLint'stype_contents_order; its body is otherwise unchanged apart from the new guard.Not included
ParserError.truncatedRecord'sexpectedMinLengthreporting the offending field's upper bound rather than the record's declared logical length was raised as an optional cleanup. Left alone: given the parameter is named min length, the field's upper bound is a defensible reading — it is the smallest record that would have contained that field — and changing it would alter the meaning of an existing public error payload for a debatable gain.Tests
Four cases added to
FixedWidthParserTests, all in the existing file:61//B/X/T, each reportinginvalidPavementClassificationwith the offending value.61/R/B/X/Tstill parsing to every expected component, so the count guard cannot silently reject valid data.fieldCountMismatchwith both counts. The "one fewer" case is the one that was never a crash — it covers the silent misread.Verification
CHANGELOG.mdgains an## [Unreleased]section. No version bump, no tag.🤖 Generated with Claude Code
https://claude.ai/code/session_01QZ3UWydnavm3g9Gs62Xirb